package ru.batrdmi.svnplugin;

import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGeometry;
import ru.batrdmi.svnplugin.logic.Revision;

import java.awt.event.KeyEvent;
import java.util.*;

public class KeyboardNavigator {
    private static final double PRECISION = 1e-3;

    private final SVNRevisionGraph graph;
    private final Map<Integer, KeyProcessor> processors = new HashMap<Integer, KeyProcessor>();

    interface KeyProcessor {
        Revision moveFrom(Revision r);
    }

    public KeyboardNavigator(SVNRevisionGraph graph) {
        this.graph = graph;
        processors.put(KeyEvent.VK_UP, new KeyProcessor() {
             @Override
             public Revision moveFrom(Revision r) {
                 return moveUp(r);
             }
         });
        processors.put(KeyEvent.VK_DOWN, new KeyProcessor() {
             @Override
             public Revision moveFrom(Revision r) {
                 return moveDown(r);
             }
         });
        processors.put(KeyEvent.VK_LEFT, new KeyProcessor() {
             @Override
             public Revision moveFrom(Revision r) {
                 return moveLeft(r);
             }
         });
        processors.put(KeyEvent.VK_RIGHT, new KeyProcessor() {
             @Override
             public Revision moveFrom(Revision r) {
                 return moveRight(r);
             }
         });
    }

    public boolean processEvent(KeyEvent e){
        KeyProcessor kp = processors.get(e.getKeyCode());
        if (kp == null) {
            return false;
        }
        List<Revision> selectedRevs = graph.getSelectedRevisions();
        if (selectedRevs.size() != 1) {
            return false;
        }
        Revision targetRevision = kp.moveFrom(selectedRevs.get(0));
        graph.selectAndShowRevision(targetRevision);
        return true;
    }

    private Revision moveUp(Revision current) {
        Set<Revision> linkedRevs = getLinkedRevisions(current);
        Revision result = null;
        Map<Revision, mxCell> cellMap = graph.getCellMap();
        double currY = cellMap.get(current).getGeometry().getY();
        Comparator<Revision> comparator = new LinkedRevisionsByDistance(current, false);
        for (Revision r : linkedRevs) {
            if ((cellMap.get(r).getGeometry().getY() - currY < PRECISION)
                    && (result == null || comparator.compare(result, r) > 0)) {
                result = r;
            }
        }
        return (result == null) ? current : result;
    }

    private Revision moveDown(Revision current) {
        Set<Revision> linkedRevs = getLinkedRevisions(current);
        Revision result = null;
        Map<Revision, mxCell> cellMap = graph.getCellMap();
        double currY = cellMap.get(current).getGeometry().getY();
        Comparator<Revision> comparator = new LinkedRevisionsByDistance(current, true);
        for (Revision r : linkedRevs) {
            if ((cellMap.get(r).getGeometry().getY() - currY > PRECISION)
                    && (result == null || comparator.compare(result, r) > 0)) {
                result = r;
            }
        }
        return (result == null) ? current : result;
    }

    private Revision moveLeft(Revision current) {
        ArrayList<Revision> revsOnPath = graph.getRevMap().get(current.getRelPath());
        ListIterator<Revision> it = revsOnPath.listIterator(revsOnPath.size());
        while (it.hasPrevious()) {
            Revision r = it.previous();
            if (r.equals(current) && it.hasPrevious()) {
                return it.previous();
            }
        }
        return current;
    }

    private Revision moveRight(Revision current) {
        ArrayList<Revision> revsOnPath = graph.getRevMap().get(current.getRelPath());
        Iterator<Revision> it = revsOnPath.iterator();
        while (it.hasNext()) {
            Revision r = it.next();
            if (r.equals(current) && it.hasNext()) {
                return it.next();
            }
        }
        return current;
    }

    private Set<Revision> getLinkedRevisions(Revision r) {
        Set<Revision> result = new HashSet<Revision>();
        for (Revision rev : graph.getCellMap().keySet()) {
            if (rev.getRelPath().equals(r.getLinkedRelPath()) && rev.getRevisionNumber() == r.getLinkedRevisionNumber()
                    || r.getRelPath().equals(rev.getLinkedRelPath()) && r.getRevisionNumber() == rev.getLinkedRevisionNumber()) {
                result.add(rev);
            }
        }
        return result;
    }

    private class LinkedRevisionsByDistance implements Comparator<Revision> {
        private final Revision reference;
        private final boolean latestIsCloser;

        public LinkedRevisionsByDistance(Revision reference, boolean latestIsCloser) {
            this.reference = reference;
            this.latestIsCloser = latestIsCloser;
        }

        @Override
        public int compare(Revision r1, Revision r2) {
            Map<Revision, mxCell> cellMap = graph.getCellMap();
            mxGeometry g = cellMap.get(reference).getGeometry();
            mxGeometry g1 = cellMap.get(r1).getGeometry();
            mxGeometry g2 = cellMap.get(r2).getGeometry();
            double diffX = Math.abs(g1.getX() - g.getX()) - Math.abs(g2.getX() - g.getX());
            if (Math.abs(diffX) > PRECISION) {
                return (diffX > 0) ? 1 : -1;
            }
            double diffY = Math.abs(g1.getY() - g.getY()) - Math.abs(g2.getY() - g.getY());
            if (Math.abs(diffY) > PRECISION) {
                return (diffY > 0) ? 1 : -1;
            }
            return (latestIsCloser ^ (r1.getRevisionNumber() > reference.getRevisionNumber())) ? 1 : -1;
        }
    }
}
